package cn.myzf.fort.access.accesscommon.processoutput.output;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Serializable;

public abstract class AbstractProcessOutput
        implements Serializable {

    /**
     * for serialization.
     */
    private static final long serialVersionUID = 1902809285333524039L;

    /**
     * the command.
     */
    protected String[] m_Command;

    /**
     * the environment variables.
     */
    protected String[] m_Environment;

    /**
     * the exit code.
     */
    protected int m_ExitCode;

    /**
     * the process.
     */
    protected transient Process m_Process;

    /**
     * the timeout for the process in seconds (ignored if < 1).
     */
    protected int m_TimeOut;

    /**
     * whether the process has timed out.
     */
    protected boolean m_TimedOut;

    /**
     * Starts the monitoring process.
     */
    public AbstractProcessOutput() {
        initialize();
    }

    /**
     * For initializing the members.
     */
    protected void initialize() {
        m_Command = new String[0];
        m_Environment = null;
        m_ExitCode = 0;
        m_Process = null;
        m_TimeOut = -1;
        m_TimedOut = false;
    }

    /**
     * Sets the timeout for the process in seconds.
     *
     * @param value the timeout (less than 1 for no timeout)
     */
    public void setTimeOut(int value) {
        if (value < 1)
            value = -1;
        m_TimeOut = value;
    }

    /**
     * Returns the timeout for the process in seconds.
     *
     * @return the timeout (less than 1 for no timeout)
     */
    public int getTimeOut() {
        return m_TimeOut;
    }

    /**
     * Performs the actual process monitoring.
     *
     * @param builder the process builder to monitor
     * @throws Exception if writing to stdin fails
     */
    public void monitor(ProcessBuilder builder) throws Exception {
        monitor(null, builder);
    }

    /**
     * Performs the actual process monitoring.
     *
     * @param builder the process builder to monitor
     * @throws Exception if writing to stdin fails
     */
    public void monitor(String input, ProcessBuilder builder) throws Exception {
        monitor(builder.command().toArray(new String[0]), null, input, builder.start());
    }

    /**
     * Performs the actual process monitoring.
     *
     * @param cmd     the command that was used
     * @param env     the environment
     * @param process the process to monitor
     * @throws Exception if writing to stdin fails
     */
    public void monitor(String cmd, String[] env, Process process) throws Exception {
        monitor(cmd, env, null, process);
    }

    /**
     * Performs the actual process monitoring.
     *
     * @param cmd     the command that was used
     * @param env     the environment
     * @param input   the input to be written to the process via stdin, ignored if null
     * @param process the process to monitor
     * @throws Exception if writing to stdin fails
     */
    public void monitor(String cmd, String[] env, String input, Process process) throws Exception {
        monitor(new String[]{cmd}, env, input, process);
    }

    /**
     * Performs the actual process monitoring.
     *
     * @param cmd     the command that was used
     * @param env     the environment
     * @param input   the input to be written to the process via stdin, ignored if null
     * @param process the process to monitor
     * @throws Exception if writing to stdin fails
     */
    public void monitor(String cmd[], String[] env, String input, Process process) throws Exception {
        m_Command = cmd;
        m_Environment = env;
        m_Process = process;
        m_TimedOut = false;

        // stderr
        Thread threade = configureStdErr(m_Process);
        threade.start();

        // stdout
        Thread threado = configureStdOut(m_Process);
        threado.start();

        // time out check
        if (m_TimeOut > 0) {
            Thread threadt = configureTimeOutMonitor(m_Process);
            threadt.start();
        }

        // writing the input to the standard input of the process
        if (input != null) {
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                    m_Process.getOutputStream()));
            writer.write(input);
            writer.close();
        }

        m_ExitCode = m_Process.waitFor();

        // wait for threads to finish
        while (threade.isAlive() || threado.isAlive()) {
            try {
                synchronized (this) {
                    wait(100);
                }
            } catch (Exception e) {
                // ignored
            }
        }

        m_Process = null;
    }

    /**
     * Configures the thread for stderr.
     *
     * @param process the process to monitor
     * @return the configured thread, not yet started
     */
    protected abstract Thread configureStdErr(Process process);

    /**
     * Configures the thread for stdout.
     *
     * @param process the process to monitor
     * @return the configured thread, not yet started
     */
    protected abstract Thread configureStdOut(Process process) throws IOException;

    /**
     * Configures the thread for watching for time outs.
     *
     * @param process the process to monitor
     * @return the configured thread, not yet started
     */
    protected Thread configureTimeOutMonitor(final Process process) {
        return new Thread(() -> {
            long start = System.currentTimeMillis();
            while (process.isAlive()) {
                try {
                    synchronized (this) {
                        wait(500);
                    }
                    // time out defined and reached?
                    if (process.isAlive()) {
                        if (((System.currentTimeMillis() - start) / 1000) >= m_TimeOut) {
                            m_TimedOut = true;
                            System.err.println("Timeout of " + m_TimeOut + " seconds reached, terminating process...");
                            process.destroy();
                            break;
                        }
                    }
                } catch (Exception e) {
                    // ignored
                }
            }
        });
    }

    /**
     * Returns the command that was used for the process.
     *
     * @return the command
     */
    public String[] getCommand() {
        return m_Command;
    }

    /**
     * Returns the environment.
     *
     * @return the environment, null if process inherited current one
     */
    public String[] getEnvironment() {
        return m_Environment;
    }

    /**
     * Returns whether the process has succeeded.
     *
     * @return true if succeeded, i.e., exit code = 0 and not timedout
     * @see #hasTimedOut()
     */
    public boolean hasSucceeded() {
        return (m_ExitCode == 0) && !hasTimedOut();
    }

    /**
     * Returns the exit code.
     *
     * @return the exit code
     */
    public int getExitCode() {
        return m_ExitCode;
    }

    /**
     * Returns whether the process timed out and got terminated.
     *
     * @return true if timedout
     * @see #getTimeOut()
     * @see #setTimeOut(int)
     */
    public boolean hasTimedOut() {
        return m_TimedOut;
    }

    /**
     * Returns the process.
     *
     * @return the process, null if not available
     */
    public Process getProcess() {
        return m_Process;
    }

    /**
     * Destroys the process if possible.
     */
    public void destroy() {
        if (m_Process != null)
            m_Process.destroy();
    }

    /**
     * Returns a short description string.
     *
     * @return the description
     */
    @Override
    public String toString() {
        return "exit code=" + m_ExitCode;
    }
}